/******************************************************************************
 * $LastChangedDate: 2012-01-05 02:09:18 -0500 (Thu, 05 Jan 2012) $
 * @file
 * @author  Jim E. Brooks  http://www.palomino3d.org
 * @brief   @brief Random number generator (sudo/real).
 *//*
 * LEGAL:   COPYRIGHT (C) 2004 JIM E. BROOKS
 *          THIS SOURCE CODE IS RELEASED UNDER THE TERMS
 *          OF THE GNU GENERAL PUBLIC LICENSE VERSION 2 (GPL 2).
 ******************************************************************************/

#define BASE_RANDOM_CC 1
#if DEBUG
#define RANDOM_VERBOSE 1
#endif
#include <cerrno>
#if OS_UNIX
#include <unistd.h>
#endif
#if OS_LINUX
#include <fcntl.h>
#endif
#if OS_WINDOWS
#include <time.h>
#else
#include <sys/time.h>
#endif
#include "base/module.hh"
#include "base/random.hh"
#if RANDOM_VERBOSE
#include "base/stream.hh"
#endif

namespace base {

////////////////////////////////////////////////////////////////////////////////
/////////////////////////////////  Random  /////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////

const int32 DEFAULT_RANDOM_SEED = 0x2b3e27bd;  // cannot be 0

bool  Random::msInit         = false;
int32 Random::msSeed         = DEFAULT_RANDOM_SEED;
int32 Random::msSeedSaved    = DEFAULT_RANDOM_SEED;
#if OS_LINUX
int   Random::msDevRandomFd  = -1;  // /dev/random
int   Random::msDevUrandomFd = -1;  // /dev/urandom
#endif

/*****************************************************************************
 * Initialize Random class.
 * NOP if called again.
 * This is automatically called by randomze().
 * Forgetting to call Init() was a common mistake
 * but calling randomize() is usually remembered.
 *****************************************************************************/
void
Random::Init( void )
{
    // Initialize once only.
    if ( msInit )
        return;
    msInit = true;

#if OS_LINUX
    // Open Linux /dev/random once.
    msDevRandomFd  = open( "/dev/random",  O_RDONLY|O_NONBLOCK );
    msDevUrandomFd = open( "/dev/urandom", O_RDONLY|O_NONBLOCK );  // suggested by man 4 random
    if ( (msDevRandomFd == -1)
     and (msDevUrandomFd == -1)  )  // if both failed
    {
        #if RANDOM_VERBOSE
        CLOG << "Random: WARNING: failed to open Linux random device file." << std::endl;
        #endif
    }
    else  // one or both were opened
    {
        #if RANDOM_VERBOSE
        CLOG << "Random: Opened Linux random device file." << std::endl;
        #endif
    }
#endif // OS_LINUX

    // Ensure int32 is really 32-bit else the self-contained random function
    // will produce different sequences of numbers.
    ASSERT( sizeof(int32) == 4 );
    ASSERT( sizeof(uint32) == 4 );
}

/*****************************************************************************
 * Seed the sequence of sudo-random numbers.
 * @param   seed
 *          If 0 is passed then picks a real random number as the seed
 *          in which case GetSeed() will return that number instead of 0.
 *****************************************************************************/
void
Random::randomize( const uint seed )
{
    // Initialize unless done.
    if ( not msInit )
        Init();

    // The random_ui() function will change mSeed every time,
    // so save the original seed for GetSeed().
    msSeed = msSeedSaved = (seed ? seed : random_ui_real());

    // Just in case.
    if ( msSeed == 0 )
        msSeed = DEFAULT_RANDOM_SEED;

#if RANDOM_VERBOSE
    CLOG << "Random: randomize seed = " << std::hex << msSeed << std::dec << std::endl;
#endif
}

/*****************************************************************************
 * @return A real random unsigned integer.
 * @remarks This function is SLOW.
 * An alternative is to call randomize() to use a real random number
 * that picks a sequence of sudo-random numbers.
 *****************************************************************************/
uint
Random::random_ui_real( void )
{
    // Try various sources of randomness.

#if OS_LINUX
    // If Linux /dev/random was opened.
    // Reading /dev/random often fails because of non-blocking mode
    // and kernel's "entropy pool" has been exhausted (man 4 random).
    // But reading /dev/urandom should succeed more often or always.

    // Try /dev/random first.
    if ( msDevRandomFd != -1 )
    {
        uint r = 0;
        if ( read( msDevRandomFd, &r, sizeof(r) ) == sizeof(r) )
            return r;
    }

    // Try /dev/urandom second.
    if ( msDevUrandomFd != -1 )
    {
        uint r = 0;
        if ( read( msDevUrandomFd, &r, sizeof(r) ) == sizeof(r) )
            return r;
    }
#endif

    // Try the time as a source of randomness.
#if OS_UNIX
    struct timeval tv;
    gettimeofday( &tv, NULL );  // POSIX
    msSeed = tv.tv_usec;        // microseconds
#elif OS_WINDOWS
    msSeed = time( 0 );         // seconds, ANSI C
#endif
    if ( (msSeed == 0) or (msSeed == -1) )  // correct seed
    {
        #if RANDOM_VERBOSE
        CLOG << "Random: WARNING: random_ui_real() isn't producing real random numbers!" << std::endl;
        #endif
        msSeed = DEFAULT_RANDOM_SEED;
    }
    return random_ui();         // msSeed affects random_ui()
}

/*****************************************************************************
 * @param   end
 *          Must be LESS THAN or EQUAL to BASE_RANDOM_MAX.
 *          Must not be 0.
 * @return  A sudo-random unsigned int in range {0,..,end-1}.
 *          end-1 is for picking a random-but-valid index within a container
 *          by passing the container's size as end.
 *          random_ui(100) will return {0,..,99} but never 100.
 *****************************************************************************/
uint
Random::random_ui( const uint end )
{
// Range cannot be greater than the maximum possible random number.
ASSERT( end <= BASE_RANDOM_MAX );  // equal is ok and quite usual
ASSERT( end > 0 );

    return random_ui() % end;  // modulo produces remainder in range {0,..,end-1}
}

/*****************************************************************************
 * @return A sudo-random fp number in range {0,..,1}.
 *****************************************************************************/
fp
Random::random_f( void )
{
  //return fp(random_ui()) / fp(BASE_RANDOM_MAX);  // slower
    return fp(random_ui()) * BASE_RANDOM_MAX_INVERSE_FP;
}

/*****************************************************************************
 * @return A sudo-random fp number in range or {0,..,range}.
 * Suggest using random_ui(range) if range is > 1.0 and random integers are usable.
 *****************************************************************************/
fp
Random::random_f( const fp range )
{
ASSERT( range > 0 );

  //return range * fp(random_ui()) / fp(BASE_RANDOM_MAX);  // slower
    return range * fp(random_ui()) * BASE_RANDOM_MAX_INVERSE_FP;
}

} // namespace base
